﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.IO;
using Skugo.Core;
using Skugo.Shared;
using System.Diagnostics;
using System.ComponentModel;
using System.Linq.Expressions;
using AvalonDock;

namespace Skugo.StandardPlugins
{
    /// <summary>
    /// A target is a root property node with a flattened hierarchy.
    /// </summary>
    public struct RemoteTarget
    {
        public RemotePropertyNode Root;
        public RemotePropertyNode[] FlattenedHierarchy;
    }

    public class PropertyGrid : DockablePlugin
    {
        /// <summary>
        /// The type of message we send or receieve.
        /// </summary>
        enum MessageType : byte
        {
            /// <summary>
            /// A snapshot of all trees (their types, children, etc).
            /// </summary>
            TreeSnapshot,

            /// <summary>
            /// A snapshot of all properties.
            /// </summary>
            PropertySnapshot,

            /// <summary>
            /// Set a particular property on the property grid.
            /// </summary>
            SetLocalProperty,

            /// <summary>
            /// Call a local method on the objects bound to the property grid.
            /// </summary>
            CallLocalMethod,

            /// <summary>
            /// Tells us whether the client is ready for the property snapshot.
            /// </summary>
            ReadyForPropertySnapshot,
        }

        /// <summary>
        /// Store all the targets with their generated or cached trees.
        /// </summary>
        private List<RemoteTarget> Targets;

        // Create a viewport form that we use to show the viewport
        private PropertyGridControl PropGrid;

        protected override void Initialize(BinaryReader reader)
        {
            base.Initialize(reader);

            // Create the form itself
            this.PropGrid = new PropertyGridControl(this);

            this.Content = this.PropGrid;

            this.SendReadyMessage();
        }

        private void SendReadyMessage()
        {
            // Create a memory stream to write data to
            MemoryStream stream = new MemoryStream();

            // Create a binary writer to write the data
            BinaryWriter outgoing = new BinaryWriter(stream);

            // Write out a the ready message
            outgoing.Write((Byte)MessageType.ReadyForPropertySnapshot);

            this.Send(stream.ToArray());
        }

        protected override void OnReceive(Byte[] data)
        {
            // Create a reader to read the data
            var incoming = data.ToBinaryReader();

            // Read the message type in
            var messageType = (MessageType)incoming.ReadByte();

            // Based on the message type...
            switch (messageType)
            {
                // On the remote side, we can get a message that communicates the entire property tree to us
                case MessageType.TreeSnapshot:
                {
                    // Read the number of trees that we're being sent (typically just one)
                    var treeCount = incoming.ReadInt32();

                    // Create a list for all flattened hierarchies
                    this.Targets = new List<RemoteTarget>();

                    // Loop through each tree and read it in from the data
                    for (Int32 i = 0; i < treeCount; ++i)
                    {
                        // Read in the tree
                        var root = this.ReadTree(incoming, (UInt32)i);

                        // Add a new target (and flatten the hierarchy)
                        this.Targets.Add(new RemoteTarget()
                        {
                            Root = root,
                            FlattenedHierarchy = FlattenTree(root),
                        });
                    }

                    // Set the root of the property grid
                    this.PropGrid.SetRoot(this.Targets[0].Root);
                    break;
                }

                // The entire state of all properties being looked at by the property tree
                case MessageType.PropertySnapshot:
                {
                    // Loop through all the targets
                    foreach (var target in this.Targets)
                    {
                        // Read all the properties for each tree (using the flattened hierarchy)
                        ReadPropertySnapshot(incoming, target.FlattenedHierarchy);
                    }

                    this.PropGrid.UpdateValues();

                    this.SendReadyMessage();
                    break;
                }

                default:
                    throw new Exception("Unhandled message type!");
            }
        }

        // Set the value of a remote variable
        public void SetRemoteValue(RemotePropertyNode node, Object value)
        {
            // Create a memory stream to write data to
            MemoryStream stream = new MemoryStream();

            // Create a binary writer to write the data
            BinaryWriter outgoing = new BinaryWriter(stream);

            // Write a safe guard mark for debugging and error checking
            outgoing.Write((Byte)MessageType.SetLocalProperty);

            // Write that we're only applying this set to an individual target
            outgoing.Write(node.MemberIndex);
            outgoing.Write(node.TargetIndex);

            // If the type is a primitive type
            if (node.Kind == NodeKind.Primitive)
            {
                // Write the value so that the other side knows what to set
                var info = TypeFactory.GetInfoFromPrimitiveType(value.GetType());
                info.ToBinary(outgoing, value);
            }
            // Otherwise if it's an enumerable type...
            else if (node.Kind == NodeKind.Enumerable)
            {
                // Our value that we're writing should always be an index
                var index = (Int32)value;

                // Write out the index directly
                outgoing.Write(index);
            }

            // Send the packet off!
            this.Send(stream.ToArray());
        }


        //private static void WriteCallParameters(BinaryWriter outgoing, Object[] parameters)
        //{
        //    // Write the number of parameters
        //    outgoing.Write((Byte)parameters.Length);

        //    // Loop through all the parameters and write them out
        //    for (int i = 0; i < parameters.Length; ++i)
        //    {
        //        // Write the parameter value
        //        outgoing.WriteDynamic(parameters[i]);
        //    }
        //}

        //// Call a remote method
        //private static Byte[] GenerateCall(UInt32 memberIndex, UInt32 targetIndex, params Object[] parameters)
        //{
        //    // Generate the packet data
        //    return Packet.Make((outgoing) =>
        //    {
        //        // Write a safe guard mark for debugging and error checking
        //        outgoing.Write((Byte)RpcMessageType.Call);

        //        // Write that we're only applying this set to an individual target
        //        WriteIndividualTarget(outgoing, memberIndex, targetIndex);

        //        // Write out all the calling parameters
        //        WriteCallParameters(outgoing, parameters);
        //    });
        //}

        //// Call a remote method
        //public void Call(UInt32 memberIndex, UInt32 targetIndex, params Object[] parameters)
        //{
        //    this.Sender(GenerateCall(memberIndex, targetIndex, parameters));
        //}

        /// <summary>
        /// Reads a property snapshot packet from a binary reader for the given target.
        /// </summary>
        /// <param name="incoming">Where we're reading the property snapshot data from.</param>
        /// <param name="flat">The flattened heirarchy in breadth first order.</param>
        private static void ReadPropertySnapshot(BinaryReader incoming, RemotePropertyNode[] flat)
        {
            // Loop through all the nodes in the flattened hierarchy
            foreach (var node in flat)
            {
                // If the node is a primitive
                if (node.Kind == NodeKind.Primitive)
                {
                    // Get type information about the current node's type
                    var typeInfo = TypeFactory.GetInfoFromPrimitiveType(node.Type);

                    // Set the value on the node
                    node.CurrentValue = typeInfo.FromBinary(incoming);
                }
                else if (node.Kind == NodeKind.Enumerable)
                {
                    // Read the enumerable index
                    var index = incoming.ReadInt32();

                    // The current value of an enumerable node is just its index
                    node.CurrentValue = index;
                }
            }
        }

        /// <summary>
        /// Flatten a property tree into a single array.
        /// </summary>
        /// <param name="root">The root of the tree.</param>
        /// <returns>Returns the flattened array of tree nodes.</returns>
        private static RemotePropertyNode[] FlattenTree(RemotePropertyNode root)
        {
            // Create a list for the flattened tree (where we store all the nodes)
            var flat = new List<RemotePropertyNode>();

            // Recursively flatten the tree
            FlattenTreeRecursive(root, flat);

            // Return the flattened array
            return flat.ToArray();
        }

        /// <summary>
        /// Recursively flatten a property tree branch by branch.
        /// </summary>
        /// <param name="branch">The current branch that we're flattening.</param>
        /// <param name="flat">The list of all nodes that we're flattening into.</param>
        private static void FlattenTreeRecursive(RemotePropertyNode branch, List<RemotePropertyNode> flat)
        {
            // If we have children...
            if (branch.Children != null)
            {
                // Loop through all the children
                foreach (var child in branch.Children)
                {
                    // Set the member index of the node
                    child.MemberIndex = (UInt32)flat.Count;

                    // Add the child to the flattened hierarchy
                    flat.Add(child);

                    // Flatten the tree recursively
                    FlattenTreeRecursive(child, flat);
                }
            }
        }


        private RemotePropertyNode ReadTree(BinaryReader incoming, UInt32 targetIndex)
        {
            // Create the root of the tree
            var root = new RemotePropertyNode();
            root.Kind = NodeKind.Expandable;

            // Read in the rest of the tree starting at the root
            this.ReadTreeBranch(incoming, root, targetIndex);

            // Return the root node
            return root;
        }

        private void ReadTreeBranch(BinaryReader incoming, RemotePropertyNode parent, UInt32 targetIndex)
        {
            // Read the number of children for the given node
            var childCount = incoming.ReadInt32();

            // Create an array to store the flattened hierarchy
            parent.Children = new RemotePropertyNode[childCount];

            // Now loop through all the child nodes
            for (Int32 i = 0; i < childCount; ++i)
            {
                // Each node can be given a name (it is optional)
                String optionalName = null;
                String optionalDescription = null;
                String optionalCategory = null;

                // Read whether the node is named or unnamed
                var info = (NodeOptions)incoming.ReadByte();

                // If the node has a name, then we need to read it
                if ((info & NodeOptions.Name) != 0)
                {
                    // Set the node's name
                    optionalName = incoming.ReadAsciiString();
                }

                // If the node has a description, then we need to read it
                if ((info & NodeOptions.Description) != 0)
                {
                    // Set the node's description
                    optionalDescription = incoming.ReadAsciiString();
                }

                // If the node has a category, then we need to read it
                if ((info & NodeOptions.Category) != 0)
                {
                    // Set the node's category
                    optionalCategory = incoming.ReadAsciiString();
                }

                // Create the child with the name
                RemotePropertyNode newChild = new RemotePropertyNode();
                newChild.OptionalName = optionalName;

                // If the node has a category, then we need to read it
                if ((info & NodeOptions.DefaultExpanded) != 0)
                {
                    newChild.DefaultExpanded = true;
                }

                // Now read in the node's type
                newChild.Kind = (NodeKind)incoming.ReadByte();

                // If the node type is a method...
                switch (newChild.Kind)
                {
                    case NodeKind.Method:
                    {
                        // Read the number of parameters
                        var parameterCount = incoming.ReadByte();

                        // Create an array for the parameter types
                        var parameterTypes = new Type[parameterCount + 1];

                        // Loop through all parameters
                        for (Int32 p = 0; p < parameterCount; ++p)
                        {
                            // Read the type guid for the current parameter type
                            var typeGuid = incoming.ReadInt64();

                            // Get the type information from the type factory
                            var typeInfo = TypeFactory.GetInfoFromPrimitiveGuid(typeGuid);

                            // Set the type for the current parameter
                            parameterTypes[p] = typeInfo.Type;
                        }

                        // We always set the return type to void
                        parameterTypes[parameterTypes.Length - 1] = typeof(void);


                        // Create a delegate type given the parameters
                        newChild.Type = Expression.GetDelegateType(parameterTypes);

                        // Make sure to set the parameter types on the node
                        newChild.ParameterTypes = parameterTypes;
                        break;
                    }

                    case NodeKind.Primitive:
                    {
                        // Read the type of the node (it's value or object type)
                        var typeGuid = incoming.ReadInt64();

                        // Otherwise, read its type from a factory and get more information about it
                        var typeInfo = TypeFactory.GetInfoFromPrimitiveGuid(typeGuid);

                        // Set its type based on what we found
                        newChild.Type = typeInfo.Type;
                        break;
                    }

                    case NodeKind.Expandable:
                    {
                        // Read the rest of the tree for the given child
                        this.ReadTreeBranch(incoming, newChild, targetIndex);
                        break;
                    }

                    case NodeKind.Enumerable:
                    {
                        // Read how many options we have
                        var count = incoming.ReadInt32();

                        // Create an array on the node to store all options
                        newChild.EnumeratedOptions = new String[count];

                        // Loop through and read each option
                        for (Int32 j = 0; j < count; ++j)
                        {
                            // Read the current option and store it in the node's option array
                            newChild.EnumeratedOptions[j] = incoming.ReadAsciiString();
                        }
                        break;
                    }

                    default:
                    {
                        throw new Exception("Invalid data type received in the property grid tree");
                    }
                }

                // Create a node and set on the hierarchy
                parent.Children[i] = newChild;

                // Set the description and category
                newChild.OptionalDescription = optionalDescription;
                newChild.OptionalCategory = optionalCategory;

                // Set the target index
                newChild.TargetIndex = targetIndex;
            }
        }
    }
}